package com.wbdca.crawler.entity;

import com.google.gson.JsonObject;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import com.wbdca.crawler.common.ConnectionPool;
import com.wbdca.crawler.common.FineGrainedTokens;
import com.wbdca.crawler.common.JsonStreamReader;
import com.wbdca.crawler.common.Message;
import com.wbdca.crawler.util.CrawlerUtil;
import lombok.Getter;
import lombok.ToString;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.net.URIBuilder;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;

@Getter
@ToString
public class User {
    private String login;
    private int id; // as of 14 Nov 2022, the maximum id of Github users is around 118,180,000.
    private String avatarUrl;
    private String url;
    private String htmlUrl;
    private String name;
    private String yearlyContribution1;
    private String yearlyContribution2;
    private String yearlyContribution3;
    private String yearlyContribution4;
    private String yearlyContribution5;
    private int publicRepos;
    private int followers;
    private int following;
    private ArrayList<Repository> repositories = new ArrayList<>();
    private ArrayList<Organization> organizations = new ArrayList<>();
    private ArrayList<Event> events = new ArrayList<>();

    public void write(String path){
        File file = new File(path + login + ".json");
        JsonWriter writer;
        try {
            writer = new JsonWriter(new FileWriter(file));
            writer.setIndent("\t");
            writeProfile(writer);
            writer.close();
        } catch (IOException e) {
            Message.warn(e.getMessage());
        }
    }

    public void writeProfile(JsonWriter writer) throws IOException {
        writer.beginObject();
        writer.name("login").value(login);
        writer.name("avatar_url").value(avatarUrl);
        writer.name("html_url").value(htmlUrl);
        writer.name("name").value(name);
        writer.name("public_repos").value(publicRepos);
        writer.name("followers").value(followers);
        writer.name("following").value(following);
        writer.name("repos");
        writeRepositories(writer);
        writer.name("organizations");
        writeOrganizations(writer);
        writer.name("yearly_contribution1").value(yearlyContribution1);
        writer.name("yearly_contribution2").value(yearlyContribution2);
        writer.name("yearly_contribution3").value(yearlyContribution3);
        writer.name("yearly_contribution4").value(yearlyContribution4);
        writer.name("yearly_contribution5").value(yearlyContribution5);
        writer.endObject();
    }

    private void writeRepositories(JsonWriter writer) throws IOException {
        writer.beginArray();
        for (Repository repository : repositories){
            System.out.println(repository.getName() + " to write");
            repository.writeProfile(writer);
        }
        writer.endArray();
    }

    private void writeOrganizations(JsonWriter writer) throws IOException {
        writer.beginArray();
        for(Organization org : organizations){
            org.writeProfile(writer);
        }
        writer.endArray();
    }

    public void updateProfile() {
        if (url == null) {
            if (login != null) {
                url = "https://api.github.com/users/" + login;
            } else if (id != 0) {
                url = "https://api.github.com/user/" + id;
            } else {
                Message.warn("User.updateProfile", "No available login or id.");
                return ;
            }
        }

        try {
            Message.debugLog("Accessing " + url);
            CloseableHttpClient client = ConnectionPool.getClient();
            HttpGet httpGet = new HttpGet(CrawlerUtil.getURI(url));
            FineGrainedTokens.addAuthorization(httpGet);
            CloseableHttpResponse response = client.execute(httpGet);
//            CloseableHttpResponse response = ConnectionPool.getResponse(new URI(url));
            JsonReader reader = new JsonReader(new InputStreamReader(response.getEntity().getContent()));
            reader.beginObject();
            while (reader.hasNext()) {
                String name = reader.nextName();
                switch (name) {
                    case "login":
                        this.login = reader.nextString();
                        break;
                    case "id":
                        this.id = reader.nextInt();
                        break;
                    case "avatar_url":
                        this.avatarUrl = reader.nextString();
                        break;
                    case "html_url":
                        this.htmlUrl = reader.nextString();
                        break;
                    case "name":
                        if (reader.peek().equals(JsonToken.NULL)) {
                            this.name = null;
                            reader.nextNull();
                        } else {
                            this.name = reader.nextString();
                        }
                        break;
                    case "public_repos":
                        this.publicRepos = reader.nextInt();
                        break;
                    case "followers":
                        this.followers = reader.nextInt();
                        break;
                    case "following":
                        this.following = reader.nextInt();
                        break;
                    default:
                        reader.skipValue();
                        break;
                }
            }
            reader.endObject();
            httpGet.reset();
        } catch (URISyntaxException | IOException e) {
            Message.warn(e.getMessage());
        }
        fetchYearlyContributions();
        Message.println("Update Profile for user " + login + " has been finished.");
    }

    public void updateRepositories(Repository repository){
        System.out.println(repository);
        this.repositories.add(repository);
    }

    public void fetchRepositories() {
        int page = 1;
        JsonReader reader;
        if (publicRepos > 3000) {
            Message.warn("User " + login + " has more than 3000 public repositories, " +
                    "the repository list will be ignored.");
            return ;
        }
        try {
            while (true) {
                CloseableHttpClient client = ConnectionPool.getClient();
                HttpGet httpGet = new HttpGet(new URIBuilder(url).appendPath("repos").
                        addParameter("page", String.valueOf(page)).build());
                FineGrainedTokens.addAuthorization(httpGet);
                CloseableHttpResponse response = client.execute(httpGet);
                reader = new JsonReader(new InputStreamReader(response.getEntity().getContent()));
                reader.beginArray();
                if (!reader.hasNext()) {
                    Message.debugLog("No more repository for " + login + " at page " + page);
                    httpGet.reset();
                    break;
                }
                Message.debugLog("Fetching repos list of " + login + " page " + page);
                while(reader.hasNext()) {
                    this.repositories.add(readRepository(reader));
                }
                reader.endArray();
                page += 1;
                httpGet.reset();
            }
            Message.debugLog(repositories.size() + " repositories of " + login + " fetched.");
        } catch (URISyntaxException | IOException e) {
            Message.warn(e.getMessage());
        }
    }

    private Repository readRepository(JsonReader reader) throws IOException {
        Repository repository = null;
        reader.beginObject();
        String firstLabel = reader.nextName();
        if (firstLabel.equals("id")) {
            repository = new Repository(reader.nextLong());
            while (reader.hasNext()) {
                String name = reader.nextName();
                switch (name) {
                    case "name":
                        repository.setName(reader.nextString());
                        break;
                    case "full_name":
                        repository.setFullName(reader.nextString());
                        break;
                    case "description":
                        if (reader.peek().equals(JsonToken.NULL)) {
                            repository.setDescription(null);
                            reader.nextNull();
                        }
                        else repository.setDescription(reader.nextString());
                        break;
                    case "url":
                        repository.setUrl(reader.nextString());
                        break;
                    case "forks":
                        repository.setForks(reader.nextInt());
                        break;
                    case "open_issues":
                        repository.setOpenIssues(reader.nextInt());
                        break;
                    case "watchers":
                        repository.setWatchers(reader.nextInt());
                        break;
                    case "license":
                        if(reader.peek().equals(JsonToken.NULL)){
                            repository.setLicense(null);
                            reader.skipValue();
                            break;
                        }
                        reader.beginObject();
                        while(reader.hasNext()){
                            String key = reader.nextName();
                            if(key.equals("name")){
                                repository.setLicense(reader.nextString());
                            } else {
                                reader.skipValue();
                            }
                        }
                        reader.endObject();
                        break;
                    case "topics":
                        reader.beginArray();
                        while(reader.hasNext()){
                            String topic = reader.nextString();
                            repository.addTopic(topic);
                        }
                        reader.endArray();
                        break;
                    default:
                        reader.skipValue();
                        break;
                }
            }
            repository.readLanguages();
            repository.fetchContributorsAmount();
        } else {
            Message.warn(login + ".readRepository", "id is not at the top, but " + firstLabel);
            while (reader.hasNext()) {
                reader.skipValue();
            }
        }
        reader.endObject();
        Message.debugLog(repository.toString());
        return repository;
    }

    public void fetchEvents() {
        int page = 1;
        try {
            while(true) {
                CloseableHttpResponse response = ConnectionPool.getResponse(new URIBuilder(this.url)
                        .appendPath("events").addParameter("page", String.valueOf(page)).build());
                JsonReader jsonReader = new JsonReader(new InputStreamReader(response.getEntity().getContent()));
                jsonReader.beginArray();
                if (!jsonReader.hasNext()) {
                    System.out.println(page + " page no content, fetch events ends! ");
                   break;
                }
                while(jsonReader.hasNext()){
                    this.events.add(readEvent(jsonReader));
                }
                jsonReader.endArray();
                page += 1;
            }
        } catch (URISyntaxException | IOException e) {
            throw new RuntimeException(e);
        }
    }

    private Event readEvent(JsonReader jsonReader) throws IOException {
        long id = 0;
        String type = null;
        String createdAt = null;

        jsonReader.beginObject();
        while (jsonReader.hasNext()) {
            String name = jsonReader.nextName();
            switch (name) {
                case "id":
                    id = jsonReader.nextLong();
                    break;
                case "type":
                    type = jsonReader.nextString();
                    break;
                case "created_at":
                    createdAt = jsonReader.nextString();
                    break;
                default:
                    jsonReader.skipValue();
                    break;
            }
        }
        jsonReader.endObject();

        if (id != 0) {
            Event event = new Event(id);
            event.setType(type);
            event.setCreatedAt(createdAt);
            return event;
        }
        return null;
    }

    public void fetchOrganizations(){
        try {
            CloseableHttpClient client = ConnectionPool.getClient();
            HttpGet httpGet = new HttpGet(new URI(this.url + "/orgs"));
            FineGrainedTokens.addAuthorization(httpGet);
            CloseableHttpResponse response = client.execute(httpGet);
            JsonReader reader = new JsonReader(new InputStreamReader(response.getEntity().getContent()));
            reader.beginArray();
            while(reader.hasNext()){
                this.organizations.add(readOrganization(reader));
            }
            reader.endArray();
            httpGet.reset();
        } catch (URISyntaxException | IOException e) {
            Message.warn(e.getMessage());
        }
    }

    private Organization readOrganization(JsonReader jsonReader) throws IOException {
        String login = null;
        int id = 0;
        String description = null;

        jsonReader.beginObject();
        while(jsonReader.hasNext()){
            String name = jsonReader.nextName();
            switch (name) {
                case "login":
                    login = jsonReader.nextString();
                    break;
                case "id":
                    id = jsonReader.nextInt();
                    break;
                case "description":
                    if (jsonReader.peek().equals(JsonToken.NULL)) {
                        description = null;
                        jsonReader.nextNull();
                    } else {
                        description = jsonReader.nextString();
                    }
                    break;
                default:
                    jsonReader.skipValue();
                    break;
            }
        }
        jsonReader.endObject();

        return new Organization(login, id, description);
    }

    private void fetchYearlyContributions() {
        String contribution = null;
        for(int year=2018;year<=2022;year++){
            try {
                Message.debugLog("Accessing " + htmlUrl);
                CloseableHttpClient client = ConnectionPool.getClient();
                HttpGet httpGet = new HttpGet(new URIBuilder(htmlUrl)
                        .addParameter("tab", "overview")
                        .addParameter("from", year+"-01-01")
                        .addParameter("from", year+"-01-31")
                        .build());
                FineGrainedTokens.addAuthorization(httpGet);
                CloseableHttpResponse response = client.execute(httpGet);
                Document document = Jsoup.parse(EntityUtils.toString(response.getEntity(), "UTF-8"));
                Elements yearlyContribution = document.getElementsByClass("f4 text-normal mb-2");
                contribution = yearlyContribution.text();
                httpGet.reset();
            } catch (URISyntaxException | ParseException | IOException | NullPointerException e) {
                Message.warn(login + ".fetchYearlyContributions", e.getMessage());
            }
            if(year==2018){
                this.yearlyContribution1 = contribution;
            }
            else if(year==2019){
                this.yearlyContribution2 = contribution;
            }
            else if(year==2020){
                this.yearlyContribution3 = contribution;
            }
            else if(year==2021){
                this.yearlyContribution4 = contribution;
            }
            else if(year==2022){
                this.yearlyContribution5 = contribution;
            }
        }
    }

    public User(JsonObject jsonObject){
        this.login = CrawlerUtil.trim(String.valueOf(jsonObject.get("login")));
        this.id = Integer.parseInt(String.valueOf(jsonObject.get("id")));
        this.avatarUrl = CrawlerUtil.trim(String.valueOf(jsonObject.get("avatar_url")));
        this.url = CrawlerUtil.trim(String.valueOf(jsonObject.get("url")));
        this.name = CrawlerUtil.trim(String.valueOf(jsonObject.get("name")));
        this.publicRepos = Integer.parseInt(String.valueOf(jsonObject.get("public_repos")));
        this.followers = Integer.parseInt(String.valueOf(jsonObject.get("followers")));
        this.following = Integer.parseInt(String.valueOf(jsonObject.get("following")));

        JsonStreamReader.updateRepositories(this);
    }

    public User(int id) {
        this.id = id;
    }

    public User(String login) {
        this.login = login;
    }
}
